Vue 源码解析之模板引擎 mustache 原理
Galloping_Leo 2021-04-17 零散笔记
# mustache
# Mustache 基础语法
let domString = Mustache.render(tempalte, data)
1
domString
是把 data
和 tempalte
整合而成的 HTML
字符串。
tempalte 的各式如下:
let tempalte = `<div>你好{{name}},我{{age}}了 {{a.b.c}}
<ul class="my-ul">
{{#students}}
<li>
<span>姓名:{{name}}</span>
<span>运算符{{ppt.user.hh}}</span>
<div>爱好</div>
<ul>
{{#hobbits}}
<li>{{.}}</li>
{{/hobbits}}
</ul>
</li>
{{/students}}
</ul>
<a href="http://wwww.baidu.com">点击</a>
<button >点击</button>
<form>
<input type="text" placeholder="输入东东..."/>
<button >提交</button>
</form>
</div>`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
data 的结构
let data = {
name: "leochenxi",
age: 12,
a: { b: { c: "a.b.c" } },
students: [
{
name: "leo11",
hobbits: ["爱好1"],
ppt: { user: { hh: "ppasdasasdasdadsppp" } },
},
{
name: "leo2",
hobbits: ["爱好2asd", "爱好5"],
ppt: { user: { hh: "ttetetetetetet" } },
},
{
name: "leo3",
hobbits: ["爱好3", "爱好6"],
ppt: { user: { hh: "nmmmmmmmmmmmmmmm" } },
},
],
hobbits: ["爱好1", "爱好3", "爱好2"],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
结果如下:
# Mustache 核心机理
- 通过实例化 Scanner 得到 scanner。
- 通过 parseTemplateToTokens,内部循环调用 scanner.scanUtil scanner.scan 两个函数,得到初步的 tokens。
- 将上一步的 tokens 经过 nestTokens 处理,得到有层级嵌套的 tokens。
- 通过 renderTemplate 将 tokens 和 data 整合成最终的 domString。
流程图如下:
# 实现原理
import Scanner from "./Scanner";
import parseTemplateToTokens from "./parseTemplateToTokens";
import nestTokens from "./nestTokens";
import renderTemplate from "./renderTemplate";
window.templateEngine = {
render(tempalteStr, data) {
let tokens = parseTemplateToTokens(tempalteStr);
let finalTokens = nestTokens(tokens);
return renderTemplate(data, finalTokens);
},
};
let data = {
name: "leochenxi",
age: 12,
a: { b: { c: "a.b.c" } },
students: [
{
name: "leo11",
hobbits: ["爱好1"],
ppt: { user: { hh: "ppasdasasdasdadsppp" } },
},
{
name: "leo2",
hobbits: ["爱好2asd", "爱好5"],
ppt: { user: { hh: "ttetetetetetet" } },
},
{
name: "leo3",
hobbits: ["爱好3", "爱好6"],
ppt: { user: { hh: "nmmmmmmmmmmmmmmm" } },
},
],
hobbits: ["爱好1", "爱好3", "爱好2"],
};
let template = templateEngine.render(
`<div>你好{{name}},我{{age}}了 {{a.b.c}}
<ul class="my-ul">
{{#students}}
<li>
<span>姓名:{{name}}</span>
<span>运算符{{ppt.user.hh}}</span>
<div>爱好</div>
<ul>
{{#hobbits}}
<li>{{.}}</li>
{{/hobbits}}
</ul>
</li>
{{/students}}
</ul>
<a href="http://wwww.baidu.com">点击</a>
<button >点击</button>
<form>
<input type="text" placeholder="输入东东..."/>
<button >提交</button>
</form>
</div>`,
data
);
console.log(template)
document.body.innerHTML = template;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// // 递归实现
// export default function nestTokens(tokens) {
// let cTokens = JSON.parse(JSON.stringify(tokens));
// let res = [];
// let j = 0;
// for (let i = 0; i < cTokens.length; i++, j++) {
// const token = cTokens[i];
// res[j] = token;
// if (token[0] === "#") {
// let nameTag = token[1];
// let endI =
// cTokens
// .slice(i)
// .findIndex((item) => item[0] == "/" && item[1] == nameTag) + i;
// let subTokens = cTokens.slice(i + 1, endI);
// res[j].push(nestTokens(subTokens));
// i = endI;
// }
// }
// return res;
// }
// tokens 折叠
// 栈实现
export default function nestTokens(tokens) {
let res = [];
let stack = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
switch (token[0]) {
case "#":
token[2] = [];
stack.push(token);
break;
case "/":
let section = stack.pop();
stack.length > 0
? stack[stack.length - 1][2].push(section)
: res.push(section);
break;
default:
if (stack.length === 0) {
res.push(token);
} else {
stack[stack.length - 1][2].push(token);
}
break;
}
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 使用 scanner 扫描生成 tokens
import Scanner from "./Scanner";
export default function (tempalteStr) {
let tokens = [];
let words;
const scanner = new Scanner(tempalteStr);
while (!scanner.eos()) {
words = scanner.scanUtil("{{");
words && tokens.push(["text", words]);
scanner.scan("{{");
words = scanner.scanUtil("}}");
if (words) {
if (words.startsWith("#")) {
tokens.push(["#", words.substring(1)]);
} else if (words.startsWith("/")) {
tokens.push(["/", words.substring(1)]);
} else {
tokens.push(["name", words]);
}
}
scanner.scan("}}");
}
return tokens;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// tokens 中 插入数据
export default function renderTemplate(data, tokens) {
let str = "";
tokens.forEach((token, index) => {
if (token[0] === "#") {
// ["#", 属性, 子tokens]
data = data[token[1]];
for (let i = 0; i < data.length; i++) {
str += renderTemplate(data[i], token[2]);
}
} else if (token[0] === "text") {
// 匹配 ["text", 字符串值]
str += token[1];
} else {
// 如果是 name
if (token[1] === ".") {
// 匹配 {{.}}
str += data;
} else if (token[1].includes(".")) {
// 匹配 {{a.b.c}}
let arrNames = token[1].split(".");
str += arrNames.reduce((a, c) => a[c], data);
} else {
// 匹配 {{ 属性名 }}
str += data[token[1]];
}
}
});
return str;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 扫描
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr;
this.pos = 0;
this.tail = this.templateStr;
}
// 走过指定内容
scan(tag) {
if (this.tail.startsWith(tag)) {
this.pos += tag.length;
this.tail = this.templateStr.substring(this.pos);
}
}
// 遇到置顶内容结束,返回扫描的内容
scanUtil(stopTag) {
const pos_backup = this.pos;
while (!this.tail.startsWith(stopTag)) {
if (this.tail.length <= 0) {
break;
}
this.pos++;
this.tail = this.templateStr.substring(this.pos);
}
return this.templateStr.substring(pos_backup, this.pos);
}
eos() {
return this.tail.length === 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31